#!/usr/bin/python
#!-*- coding:utf-8 -*-
# Copyright 2011 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import mechanize
import optparse
import os
import re
import signal
import subprocess
import sys
import threading
import time
import traceback
import unittest
import urlparse

import mock_pfserver
import remote_api


class ProcessRunner(threading.Thread):
    """A thread that starts a subprocess, collects its output, and stops it."""

    READY_RE = re.compile('')  # this output means the process is ready
    OMIT_RE = re.compile('INFO ')  # omit these lines from the displayed output
    ERROR_RE = re.compile('ERROR|CRITICAL')  # this output indicates failure

    def __init__(self, name, args):
        threading.Thread.__init__(self)
        self.name = name
        self.args = args
        self.process = None  # subprocess.Popen instance
        self.ready = False  # process is running and ready
        self.failed = False  # process emitted an error message in its output
        self.output = []

    def run(self):
        """Starts the subprocess and collects its output while it runs."""
        self.process = subprocess.Popen(
            self.args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            close_fds=True)

        # Each subprocess needs a thread to be watching it and absorbing its
        # output; otherwise it will block when its stdout pipe buffer fills.
        while self.process.poll() is None:
            line = self.process.stdout.readline()
            if not line:  # process finished
                return
            if self.READY_RE.search(line):
                self.ready = True
            if self.OMIT_RE.search(line):  # filter out these lines
                continue
            if self.ERROR_RE.search(line):  # something went wrong
                self.failed = True
            if line.strip():
                self.output.append(line.strip())

    def stop(self):
        """Terminates the subprocess and returns its status code."""
        if self.process:  # started
            if self.isAlive():  # still running
                os.kill(self.process.pid, signal.SIGKILL)
            else:
                self.failed = self.process.returncode != 0
        self.clean_up()
        if self.failed:
            self.flush_output()
            print >>sys.stderr, '%s failed (status %s).\n' % (
                self.name, self.process.returncode)
        else:
            print >>sys.stderr, '%s stopped.' % self.name

    def flush_output(self):
        """Flushes the buffered output from this subprocess to stderr."""
        self.output, lines_to_print = [], self.output
        if lines_to_print:
            print >>sys.stderr
        for line in lines_to_print:
            print >>sys.stderr, self.name + ': ' + line

    def wait_until_ready(self, timeout=10):
        """Waits until the subprocess has logged that it is ready."""
        fail_time = time.time() + timeout
        while self.isAlive() and not self.ready and time.time() < fail_time:
            for jiffy in range(10):  # wait one second, aborting early if ready
                if not self.ready:
                    time.sleep(0.1)
            if not self.ready:
                self.flush_output()  # after each second, show output
        if self.ready:
            print >>sys.stderr, '%s started.' % self.name
        else:
            raise RuntimeError('%s failed to start.' % self.name)

    def clean_up(self):
        pass


class AppServerRunner(ProcessRunner):
    """Manages a dev_appserver subprocess."""

    READY_RE = re.compile('Running application ' + remote_api.get_app_id())

    def __init__(self, port, pfserver_port):
        # TODO: Pass pfserver_port to appengine.
        ProcessRunner.__init__(self, 'pf_mobile', [
            os.environ['PYTHON'],
            os.path.join(os.environ['APPENGINE_DIR'], 'dev_appserver.py'),
            os.environ['APP_DIR'],
            '--port=%s' % port,
            '--clear_datastore'
        ])

    def clean_up(self):
      pass


class ThreadedServerRunner(threading.Thread):
  def __init__(self, server):
    threading.Thread.__init__(self)
    self.server = server

  def run(self):
    self.server.run()

  def stop(self):
    self.server.shutdown()

  def wait_until_ready(self, timeout=10):
    pass

  def flush_output(self):
    pass


class TestsBase(unittest.TestCase):
    """Base class for test cases."""
    verbose = 0
    hostport = None
    kinds_written_by_tests = []

    def setUp(self):
        """Sets up a scrape Session for each test."""
        # See http://zesty.ca/scrape for documentation on scrape.
        #self.s = scrape.Session(verbose=self.verbose)
        self.br = mechanize.Browser()

        # pf_mobile cause error first time, so try to access first.
        try:
          self.go('/')
        except Exception, inst:
          #print inst
          pass

    def go(self, path):
        """Navigates the scrape Session to the given path on the test server."""
        #return self.s.go('http://' + self.hostport + path, **kwargs)
        return self.br.open('http://' + self.hostport + path)

    def tearDown(self):
        """Resets the datastore by deleting anything written during a test."""
        pass

    def LinkExist(self, path, text):
      """Checks current page has specific link."""
      target_url = 'http://' + self.hostport + path
      for link in self.br.links():
        url = urlparse.urljoin(link.base_url, link.url)
        if url == target_url:
          return link.text.decode('sjis') == text
      return false

    def FollowLink(self, text_regex):
      self.br.follow_link(text_regex = text_regex.encode('shift-jis'))


class ReadOnlyTests(TestsBase):
  """Tests that don't modify data go here."""

  def test_main(self):
    """Check the main page with no language specified."""
    doc = self.go('/')
    assert self.LinkExist('/search', u'人を探す')
    assert self.LinkExist('/create', u'生存者を登録する')

  def test_seach(self):
    """Check the main page with no language specified."""
    doc = self.go('/')
    self.FollowLink(u'人を探す')
    self.br.select_form(nr=0)
    self.br['q'] = 'test'
    self.br.submit()
    # check search result



def main():
  parser = optparse.OptionParser()
  parser.add_option('-a', '--address', default='localhost',
                    help='appserver hostname (default: localhost)')
  parser.add_option('-p', '--port', type='int', default=8080,
                    help='appserver port number (default: 8080)')
  parser.add_option('-q', '--pfport', type='int', default=8081,
                    help='PersonFinder server port number (default: 8081)')
  parser.add_option('-v', '--verbose', action='store_true')
  options, args = parser.parse_args()

  try:
    threads = []
    pfserver = mock_pfserver.MockPfServer(options.pfport)
    threads.append(ThreadedServerRunner(pfserver))
    threads.append(AppServerRunner(options.port, options.pfport))
    for thread in threads:
      thread.start()
    for thread in threads:
      thread.wait_until_ready(100)

    # Connect to the datastore.
    hostport = '%s:%d' % (options.address, options.port)
    remote_api.connect(hostport, remote_api.get_app_id(), 'test', 'test')
    TestsBase.hostport = hostport
    TestsBase.verbose = options.verbose

    unittest.main()  # You can select tests using command-line arguments.

  except Exception, e:
    # Something went wrong during testing.
    for thread in threads:
      thread.flush_output()
    traceback.print_exc()
    raise SystemExit
  finally:
    for thread in threads:
      thread.stop()
      thread.join()

if __name__ == '__main__':
    main()
